// **********************************************************************
//
// <copyright>
//
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
//
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/rpf/RpfTocHandler.java,v $
// $RCSfile: RpfTocHandler.java,v $
// $Revision: 1.16 $
// $Date: 2006/12/13 16:45:24 $
// $Author: dietrick $
//
// **********************************************************************
/**
 * Modifications: 1. Changed getBestCoverageEntry() to consider more than one zone. 2. Changed getBestCoverageEntry() to
 * return multiple entries.
 */

/*
 * The meat of this code is based on source code provided by The MITRE
 * Corporation, through the browse application source code.  Many
 * thanks to Nancy Markuson who provided BBN with the software, and to
 * Theron Tock, who wrote the software, and Daniel Scholten, who
 * revised it - (c) 1994 The MITRE Corporation for those parts, and
 * used/distributed with permission.  The RPF TOC reading mechanism is
 * the contributed part.
 */
package com.bbn.openmap.layer.rpf;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Vector;

import com.bbn.openmap.io.BinaryBufferedFile;
import com.bbn.openmap.io.BinaryFile;
import com.bbn.openmap.io.FormatException;
import com.bbn.openmap.proj.CADRG;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.util.Debug;

/**
 * The RpfTocHandler knows how to read A.TOC files for RPF raster data. The A.TOC file describes the coverage found in
 * the tree of data that accompanies it. This coverage is described as a series of rectangles describing the frame of
 * groups of coverage, with common-scale maps, types for different CADRG zones. The RpfTocHandler can also provide a
 * description of the frames and subframes to use for a screen with a given projection.
 * <P>
 *
 * The RPF specification says that the frame paths and file names, from the RPF directory, should be in upper-case
 * letters. The paths and file names are stored in the A.TOC file this way. Sometimes, however, through CDROM and
 * downloading quirks, the paths and file names, as stored on the hard drive, are actually transferred to lower-case
 * letters. This RpfTocHandler will check for lower case letter paths, but only for all the letters to be lower case.
 * The frame will be marked as non-existent if some of the directories or filenames have be transformed to uppercase.
 */
public class RpfTocHandler {

	public final static String RPF_TOC_FILE_NAME = "A.TOC";
	public final static String LITTLE_RPF_TOC_FILE_NAME = "a.toc";
	public final static int DEFAULT_FRAME_SPACE = 300; // frame file
	// in kilobytes

	protected RpfHeader head;
	protected String aTocFilePath;
	protected boolean aTocByteOrder;
	protected BinaryFile binFile;
	protected RpfFileSections.RpfLocationRecord[] locations;
	/**
	 * The boundary rectangles in the A.TOC file.
	 */
	protected RpfTocEntry[] entries;
	protected String dir;
	protected boolean Dchum;
	protected long estimateDiskSpace; // uint
	protected int numBoundaries;
	protected long numFrameIndexRecords; // uint # frame file index records
	protected int indexRecordLength; // ushort, frame file index record
	// length
	protected long currencyTime;
	protected boolean valid = false;
	/**
	 * Set by the RpfFrameProvider, and used to track down this particular TOC to get to the frames offered by it's
	 * coverages.
	 */
	private int tocNumber = 0;

	/**
	 * Flag to tell the TOC handler to not consider matching zones when evaluating coverage boxes for which one provides
	 * the best coverage for a projection. Because the subframes are being scaled and warped, it's better to get
	 * coverage from any zone rather than to limit the entry responses with zone matching the projection.
	 */
	private boolean ignoreZonesForCoverageBoxes = true;

	/**
	 * Flag to note whether absolute pathnames are used in the A.TOC. Set to false, because it's not supposed to be that
	 * way, according to the specification. This is reset automatically when the A.TOC file is read. If the first two
	 * characters of the directory paths are ./, then it stays false.
	 */
	protected boolean fullPathsInATOC = false;

	protected boolean DEBUG_RPF = false;
	protected boolean DEBUG_RPFTOC = false;
	protected boolean DEBUG_RPFTOCDETAIL = false;
	protected boolean DEBUG_RPFTOCFRAMEDETAIL = false;

	// Added zone extents
	private static final int CADRG_zone_extents[] = {
		0,
		32,
		48,
		56,
		64,
		68,
		72,
		76,
		80,
		90
	};

	public RpfTocHandler() {

		DEBUG_RPF = Debug.debugging("rpf");
		DEBUG_RPFTOC = Debug.debugging("rpftoc");
		DEBUG_RPFTOCDETAIL = Debug.debugging("rpftocdetail");
		DEBUG_RPFTOCFRAMEDETAIL = Debug.debugging("rpftocframedetail");

		estimateDiskSpace = DEFAULT_FRAME_SPACE;

		if (Debug.debugging("rpftoc")) {
			Debug.error("RpfTocHandler: No TOC parent directory name in constructor");
		}
	}

	/**
	 * Should be used in situations where it is certain that this is the only A.TOC in town.
	 */
	public RpfTocHandler(String parentDir) {
		this(parentDir, 0);
	}

	/**
	 * Used when there is more than one A.TOC being used, or where there is a possibility of that happening, like in the
	 * RPF layer. The TOC number should be unique for a certain RpfFrameProvider.
	 *
	 * @param parentDir the RPF directory
	 * @param TOCNumber a unique number to identify this TOC for a RpfFrameProvider.
	 */
	public RpfTocHandler(String parentDir, int TOCNumber) {
		tocNumber = TOCNumber;
		estimateDiskSpace = DEFAULT_FRAME_SPACE;

		/* DKS. Open input "A.TOC" */
		valid = loadFile(parentDir);
		if (!valid) {
			Debug.error("RpfTocHandler: Invalid TOC File in " + parentDir);
		}
	}

	/**
	 * Given a parent RPF directory, find the a.toc file directly inside it, as dictated by the specification. Not
	 * called anymore - the BinaryFile does the searching, and can find URL and jar files.
	 *
	 * @param parentDir Path to the RPF directory.
	 * @return File
	 */
	public File getTocFile(String parentDir) {
		/* DKS. Open input "A.TOC" */
		File file = new File(parentDir + "/" + RPF_TOC_FILE_NAME);
		if (!file.exists()) {
			file = new File(parentDir + "/" + LITTLE_RPF_TOC_FILE_NAME);
			if (!file.exists()) {
				// Debug.error("RpfTocHandler: getTocFile(): file in
				// "+
				// parentDir + " not found");
				return null;
			}
		}

		if (DEBUG_RPFTOCDETAIL) {
			Debug.output("RpfTocHandler: getTocFile(): TOC file is " + file);
		}

		return file;
	}

	/**
	 * True if the A.TOC file is readable/present/good.
	 */
	public boolean isValid() {
		return valid;
	}

	/**
	 * A way to check if the status of the A.TOC file is different, in case another one has taken its place. Handy if
	 * the A.TOC is on a CDROM drive and the disk has been swapped. Not valid anymore, with the advent of the new
	 * BinaryFile, where the file information may not be available.
	 */
	public boolean hasChanged() {
		// File tmpFile = getTocFile(dir);
		// if (tmpFile == null) {
		// return valid;
		// }
		// if (tmpFile.lastModified() != currencyTime && valid) {
		// valid = false;
		// return true;
		// }
		return false;
	}

	/**
	 * Re-read the A.TOC file in the parent directory.
	 */
	public boolean reload() {
		return loadFile(dir);
	}

	/**
	 * Read the file and load its parameters into this object.
	 */
	public boolean loadFile(String parentDir) {

		boolean ret = true;

		String upperCaseVersion = parentDir + "/" + RPF_TOC_FILE_NAME;
		String lowerCaseVersion = parentDir + "/" + LITTLE_RPF_TOC_FILE_NAME;

		try {

			if (BinaryFile.exists(upperCaseVersion)) {
				binFile = new BinaryBufferedFile(upperCaseVersion);
				aTocFilePath = upperCaseVersion;
			} else if (BinaryFile.exists(lowerCaseVersion)) {
				binFile = new BinaryBufferedFile(lowerCaseVersion);
				aTocFilePath = lowerCaseVersion;
			}

			if (binFile == null) {
				return false;
			}

			if (DEBUG_RPFTOC) {
				Debug.output("RpfTocHandler: TOC file is in " + parentDir);
			}

			dir = parentDir + "/";

			// With the new BinaryFile, we can't get to this
			// info, because we aren't using File objects anymore.
			// currencyTime = file.lastModified();
			if (!parseToc(binFile)) {
				ret = false;
				Debug.error("RpfTocHandler: loadFile(): error parsing A.TOC file!!");
			}

			aTocByteOrder = binFile.byteOrder();

			binFile.close();
		} catch (IOException e) {
			ret = false;
		}
		binFile = null;
		return ret;
	}

	protected boolean parseToc(BinaryFile binFile) {

		if (DEBUG_RPFTOC) {
			Debug.output("ENTER TOC parsing...");
		}

		try {
			// binFile should be set to the beginning at this point
			binFile.seek(0);

			// Read header
			head = new RpfHeader();
			if (!head.read(binFile)) {
				return false;
			}

			if (DEBUG_RPFTOC) {
				Debug.output("RpfTocHandler.parseToc: read header:\n" + head);
			}

			binFile.seek(head.locationSectionLocation);
			RpfFileSections rfs = new RpfFileSections(binFile);

			// Everything must be OK to reach here...
			// DKS. fseek to start of location section: 48
			// DFD not necessarily 48! New A.TOCs are different.
			locations = rfs.getLocations(RpfFileSections.TOC_LOCATION_KEY);

			// Read boundary rectangles
			// Number of Boundary records
			// DKS: now phys_index, not index
			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): fseek to Boundary section subheader: "
					+ locations[0].componentLocation);
			}

			binFile.seek(locations[0].componentLocation);

			// NEW
			long boundRectTableOffset = (long) binFile.readInteger();

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): BoundRectTableOffset: " + boundRectTableOffset);
			}

			int n = (int) binFile.readShort();
			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): # Boundary rect. recs: " + n);
			}

			numBoundaries = n;

			// DKS new
			// Boundary record length
			int boundaryRecordLength = (int) binFile.readShort();

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): should be 132: " + boundaryRecordLength);
			}

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): fseek to Boundary Rectangle Table: "
					+ locations[1].componentLocation);
			}
			binFile.seek(locations[1].componentLocation);

			entries = new RpfTocEntry[numBoundaries];

			// Read Boundary rectangle records
			for (int i = 0; i < n; i++) {
				if (DEBUG_RPFTOCDETAIL) {
					Debug.output("RpfTocHandler: parseToc(): read boundary rec#: " + i);
				}

				// All this stuff moved to RpfTocEntry.java - DFD
				// 8/18/99
				entries[i] = new RpfTocEntry(binFile, tocNumber, i);

				if (DEBUG_RPFTOCDETAIL) {
					Debug.output("RpfTocHandler: parseToc(): entry " + i + " has scale " + entries[i].scale + ", type "
						+ (entries[i].Cib ? "CIB" : "CADRG") + " in zone " + entries[i].zone);
					if (entries[i].Cib) {
						Debug.output("RpfTocHandler: parseToc(): entry noted as a Cib entry.");
					}
				}
			}

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): Read frame file index section subheader at loc: "
					+ locations[2].componentLocation);
			}

			// Read # of frame file index records
			// Skip 1 byte security classification
			// locations[2] is loc of frame file index section
			// subheader
			binFile.seek(locations[2].componentLocation + 1);

			// NEW
			long frameIndexTableOffset = (long) binFile.readInteger();
			numFrameIndexRecords = (long) binFile.readInteger();
			int numPathnameRecords = (int) binFile.readShort();
			// indexRecordLength should now be 33, not 35
			indexRecordLength = (int) binFile.readShort();

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): frameIndexTableOffset: " + frameIndexTableOffset);
				Debug.output("RpfTocHandler: parseToc(): # Frame file index recs: " + numFrameIndexRecords);
				Debug.output("RpfTocHandler: parseToc(): # pathname records: " + numPathnameRecords);
				Debug.output("RpfTocHandler: parseToc(): Index rec len(33): " + indexRecordLength);
			}

			// In previous version of the RpfTocHandler, we went ahead and read
			// in all of the RPF frame file paths. For very large collections of
			// data, expecially with large scale charts and imagery, this turns
			// out to use a huge amount of data. The code has been reorganized
			// to skip this part until it's been determined that those files are
			// actually going to be used by the layer.
			// readFrameInformation(binFile);
			// One thing that we still need to do at this point, is query some
			// of the frame file paths to find out what chart type is being held
			// for each RpfTocEntry, so during the coverage determination we can
			// decide whether to use an RpfTocEntry or not.
			figureOutChartSeriesForEntries(binFile);

		} catch (IOException ioe) {
			Debug.error("RpfTocHandler: IO ERROR parsing file!\n\t" + ioe);
			return false;
		} catch (FormatException fe) {
			Debug.error("RpfTocHandler: Format ERROR parsing file!\n\t" + fe);
			return false;
		}

		if (DEBUG_RPFTOC) {
			Debug.output("LEAVE TOC parsing...");
		}
		return true;
	}

	/**
	 * Method that looks at one frame file for each RpfTocEntry, in order to check the suffix and load the chart series
	 * information into the RpfTocEntry. This is needed for when the RpfTocHandler is asked for matching RpfTocEntries
	 * for a given scale and location when the chart type has been limited to a certain chart code.
	 *
	 * @param binFile
	 * @throws IOException
	 * @throws FormatException
	 */
	protected void figureOutChartSeriesForEntries(BinaryFile binFile)
		throws IOException, FormatException {

		RpfTocEntry[] entriesAlreadyChecked = new RpfTocEntry[entries.length];
		System.arraycopy(entries, 0, entriesAlreadyChecked, 0, entries.length);

		// We just need the name of one file, just to see what the series code
		// is.
		for (int i = 0; i < numFrameIndexRecords; i++) {
			// Read frame file index records
			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): Read frame file index rec #: " + i);
			}

			// Index_subhdr_len (9) instead of table_offset (11)
			// indexRecordLength (33) instead of 35
			// componentLocation, not index
			// locations[3] is frame file index table subsection
			binFile.seek(locations[3].componentLocation + indexRecordLength * i);

			int boundaryId = (int) binFile.readShort();

			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("boundary id for frame: " + i + " is " + boundaryId);
			}

			if (boundaryId > numBoundaries - 1) {
				throw new FormatException("Bad boundary id in FF index record " + i);
			}

			RpfTocEntry entry = entriesAlreadyChecked[boundaryId];

			if (entry == null) {
				continue; // already checked.
			} else {
				entriesAlreadyChecked[boundaryId] = null;
			}

			/* int frameRow = (int) */ binFile.readShort();
			/* int frameCol = (int) */
			binFile.readShort();
			/* long pathOffset = (long) */
			binFile.readInteger();
			String filename = binFile.readFixedLengthString(12);

			// Figure out the chart series ID
			int dot = filename.lastIndexOf('.');
			// Interned so we can look it up in the catalog
			// later...
			entry.setInfo(filename.substring(dot + 1, dot + 3).intern());
		} /* for i = numFrameIndexRecords */

	}

	/**
	 * Should be called by the RpfFrameCacheHandler before any frame files are loaded from a RpfTocEntry. The
	 * RpfFrameCacheHandler should ask the RpfTocEntry if the frames have been loaded, and call this if they have not.
	 */
	protected void loadFrameInformation(RpfTocEntry rpfTocEntry) {
		try {
			if (binFile == null && aTocFilePath != null) {
				binFile = new BinaryBufferedFile(aTocFilePath);
				binFile.byteOrder(aTocByteOrder);
				readFrameInformation(binFile, rpfTocEntry);
				binFile.close();
				binFile = null;
			}
		} catch (IOException ioe) {
			Debug.error("RpfTocHandler: IO ERROR parsing file for frame information!\n\t" + ioe);
		} catch (FormatException fe) {
			Debug.error("RpfTocHandler: Format ERROR parsing file for frame information!\n\t" + fe);
		}
	}

	/**
	 * Reads the BinaryFile to retrieve the Frame file information for the entry.
	 *
	 * @param binFile a valid, open BinaryFile.
	 * @param entry the RpfTocEntry to fill.
	 * @throws IOException
	 * @throws FormatException
	 */
	protected void readFrameInformation(BinaryFile binFile, RpfTocEntry entry)
		throws IOException, FormatException {
		int boundaryId, frameRow, frameCol; // ushort
		int currentPosition;
		int pathLength;
		long pathOffset; // uint, offset of frame file pathname
		RpfFrameEntry frame;

		int currentBoundaryIdForEntry = entry.coverage.entryNumber;

		// Read frame file index records
		for (int i = 0; i < numFrameIndexRecords; i++) {
			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): Read frame file index rec #: " + i);
			}

			// Index_subhdr_len (9) instead of table_offset (11)
			// indexRecordLength (33) instead of 35
			// componentLocation, not index
			// locations[3] is frame file index table subsection
			binFile.seek(locations[3].componentLocation + indexRecordLength * i);

			boundaryId = (int) binFile.readShort();

			if (boundaryId != currentBoundaryIdForEntry) {
				// Only load the frame names of the entry we are using...
				continue;
			}

			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("boundary id for frame: " + i + " is " + boundaryId);
			}

			// DKS NEW: changed from 1 to 0 to agree w/ spec. -1
			// added also.
			// if (boundaryId < 0 || boundaryId > numBoundaries -
			// 1 )
			if (boundaryId > numBoundaries - 1) {
				throw new FormatException("Bad boundary id in FF index record " + i);
			}

			frameRow = (int) binFile.readShort();
			frameCol = (int) binFile.readShort();

			// DKS. switched from horizFrames to vertFrames
			// DKS NEW: CHANGED FROM 1 to 0 to agree w/spec. ALSO
			// COL below
			// if (frameRow < 1 || frameRow > entry->vertFrames)
			if (frameRow > entry.vertFrames - 1) {
				throw new FormatException("Bad row number: " + frameRow + ", in FF index record " + i
					+ ", Min row num=0;  Max. row num:" + (entry.horizFrames - 1));
			}

			// DKS. switched from vertFrames to horizFrames
			if (frameCol > entry.horizFrames - 1) {
				throw new FormatException(" Bad col number in FF index record " + i);
			}

			// DKS NEW: -1 removed on frameRow, col
			// JRB
			// frame = &entry->frames[frameRow][frameCol];
			// [(entry->vertFrames - 1L)-frameRow] flips the array
			// over, so that the frames can be referenced
			// correctly from the top left, instead of the
			// specification notation of bottom left.
			frame = entry.getFrame((entry.vertFrames - 1) - frameRow, frameCol);

			if (frame.exists && DEBUG_RPFTOCDETAIL) {
				Debug.output("FF " + i + " is a duplicate");
			}

			// DKS: phys_loc deleted
			// pathname offset
			pathOffset = (long) binFile.readInteger();

			if (pathOffset < 0) {
				continue;
			}

			// Save file position for later
			currentPosition = (int) binFile.getFilePointer();

			// Go to start of pathname record
			// DKS. New pathOffset offset from start of frame file
			// index section of TOC??
			// DKS. Add pathoffset wrt frame file index table
			// subsection (loc[3])
			binFile.seek(locations[3].componentLocation + pathOffset);

			pathLength = (int) binFile.readShort();
			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): pathLength:" + pathLength);
			}

			// 1st part of directory name is passed as arg:
			// e.g. "../RPF2/"
			String rpfdir = dir;
			StringBuffer sBuf = new StringBuffer(pathLength);

			// read rest of directory name from toc
			// DKS: skip 1st 2 chars: "./":
			String pathTest = binFile.readFixedLengthString(2);
			if (pathTest.equals("./")) {
				fullPathsInATOC = false;
			} else {
				fullPathsInATOC = true;
			}

			if (!fullPathsInATOC) {
				// DKS: Make up for skipped 2 chars
				sBuf.append(binFile.readFixedLengthString(pathLength - 2));
			} else {
				sBuf.append(pathTest);
				sBuf.append(binFile.readFixedLengthString(pathLength - 2));
			}

			// Add the trim because it looks like NIMA doesn't
			// always get the pathLength correct...
			String directory = sBuf.toString().trim();
			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): frame directory: " + directory);
			}

			/* Go back to get filename tail */
			binFile.seek(currentPosition);

			String filename = binFile.readFixedLengthString(12);
			if (DEBUG_RPFTOCFRAMEDETAIL) {
				Debug.output("RpfTocHandler: parseToc(): frame filename: " + filename);
			}

			// Figure out the chart series ID
			int dot = filename.lastIndexOf('.');
			// Interned so we can look it up in the catalog
			// later...
			entry.setInfo(filename.substring(dot + 1, dot + 3).intern());

			// We duplicate this below!!!
			// frame.framePath = new String(frame.rpfdir +
			// frame.directory +
			// "/" + frame.filename);
			// DKS new DCHUM. Fill in last digit v of vv version
			// #. fffffvvp.JNz or ffffffvp.IMz for CIB boundaryId
			// will equal frame file number: 1 boundary rect. per
			// frame.
			// if (Dchum)
			// entries[boundaryId].version =
			// frame.filename.charAt(6);
			String tempPath;

			if (!fullPathsInATOC) {
				tempPath = rpfdir + directory + filename;
				frame.rpfdirIndex = (short) (rpfdir.length() - 3);
				frame.filenameIndex = (short) (rpfdir.length() + directory.length());
			} else {
				tempPath = directory + filename;
				frame.filenameIndex = (short) directory.length();
			}

			frame.framePath = tempPath;
			frame.exists = true;

			// You don't want to check for the existance of frames here, do
			// it at load time, and then mark the entry if the load fails.
			// If you do the check here, you waste a lot of I/O time and
			// effort. Assume it's there, the RPFFrame has been modified
			// to try lower case names if needed.
			if (frame.framePath == null) {
				Debug.output("RpfTocHandler: Frame " + tempPath
					+ " doesn't exist.  Please rebuild A.TOC file using MakeToc, or check read permissions for the file.");
			}
		} /* for i = numFrameIndexRecords */

	}

	/**
	 * Util-like function that translates a long to the string representation found in the A.TOC file.
	 */
	public static String translateScaleToSeries(long scale) {
		if (scale == 0) {
			return "Various    ";
		} else if (scale == 50000L) {
			return "1:50K      ";
		} else if (scale == 100000L) {
			return "1:100K     ";
		} else if (scale == 200000L) {
			return "1:200K     ";
		} else if (scale == 250000L) {
			return "1:250K     ";
		} else if (scale == 500000L) {
			return "1:500K     ";
		} else if (scale == 1000000L) {
			return "1:1M       ";
		} else if (scale == 2000000L) {
			return "1:2M       ";
		} else if (scale == 5000000L) {
			return "1:5M       ";
		} else if (scale == 66666L) {
			return "10M         ";
		} else if (scale == 33333L) {
			return "5M          ";
		} else {
			return (String) null;
		}
	}

	/**
	 * Given the scale string found in the A.TOC file, decode it into a 'long' scale.
	 */
	public static long textScaleToLong(String textScale) {

		long resolution = 1l;
		long realValue;
		int expLetter; // location of m, M, K
		int expLetterSmall;

		// Make sure there are no commas, commas seem to kill Long parsing.
		int commaIndex = textScale.indexOf(',');
		while (commaIndex != -1) {
			StringBuffer buf = new StringBuffer(textScale.substring(0, commaIndex));
			buf.append(textScale.substring(commaIndex + 1));
			textScale = buf.toString();
			commaIndex = textScale.indexOf(',');
		}

		int colon = textScale.indexOf(":");

		try {
			if (colon == -1) {
				// dealing with an imagery scale
				expLetter = textScale.indexOf("m");
				if (expLetter == -1) {
					expLetter = textScale.indexOf("M");
				}

				if (expLetter != -1) {
					resolution = Long.parseLong(textScale.substring(0, expLetter));
					return (long) (resolution / .000150);
				}

				// If we get here, we're dealing with a chart scale that doesn't
				// have a 1: at the front of it, so continue on...
			}

			// dealing with a map scale
			String expValue = "";

			expLetter = textScale.lastIndexOf('K');
			expLetterSmall = textScale.lastIndexOf('k');

			if (expLetter == -1 && expLetterSmall == -1) {
				expLetter = textScale.lastIndexOf('M');
				expLetterSmall = textScale.lastIndexOf('m');

				if (expLetter != -1 || expLetterSmall != -1) {
					expValue = "000000";
				}
			} else {
				expValue = "000";
			}

			StringBuffer buf;
			if (expValue.length() > 0) {
				// make sure we have the right index variable
				if (expLetter == -1) {
					expLetter = expLetterSmall;
				}
				// If there isn't a colon, this should be OK
				buf = new StringBuffer(textScale.substring(colon + 1, expLetter));
				buf.append(expValue);
			} else {
				buf = new StringBuffer(textScale.substring(colon + 1));
			}

			String longString = buf.toString().trim();
			realValue = Long.parseLong(longString);

		} catch (NumberFormatException nfe) {
			if (Debug.debugging("rpftoc")) {
				Debug.output("textScaleToLong: Number Format Exception!!!!" + textScale);
			}
			return (long) RpfConstants.Various;
		} catch (StringIndexOutOfBoundsException sioobe) {
			if (Debug.debugging("rpftoc")) {
				Debug.output("textScaleToLong: String index out of bounds:\n" + sioobe.getMessage());
			}
			return (long) RpfConstants.Various;
		}

		if (colon != -1) {
			resolution = Long.parseLong(textScale.substring(0, colon));
		}

		long ret = (realValue / resolution);

		if (Debug.debugging("rpftoc")) {
			Debug.output("RpfTocHandler: textScaleToLong converted " + textScale + " to " + ret);
		}

		return ret;

	}

	protected int getASCIIZone(double ullat, int zone) {
		int z = zone;
		// Now convert it to ASCII to compare
		if (ullat > 0) {
			z += 48; // for ASCII compare next
		} else {
			z += 64;
			if (z == 73) {
				z++; // Can't be equal to I -> J
			}
		}
		return z;
	}

	/**
	 * Given a coordinate box and a scale, return the entries that have coverage over the given area. The chart types
	 * returned are dictated by the chartSeriesCode passed in, which must be an entry from an RpfProductInfo.seriesCode.
	 *
	 * @param ullat upper left latitude, in decimal degrees
	 * @param ullon upper left longitude, in decimal degrees
	 * @param lrlat lower right latitude, in decimal degrees
	 * @param lrlon lower right longitude, in decimal degrees
	 * @param proj CADRG projection describing map.
	 * @param chartSeriesCode chart selection. If null, all coverage boxes fitting on the screen will be returned.
	 * @param coverages a list of potential coverages
	 */
	public void getCatalogCoverage(double ullat, double ullon, double lrlat, double lrlon, Projection proj,
		String chartSeriesCode,
		Vector<RpfCoverageBox> coverages) {
		if (!valid) {
			return;
		}

		String chartSeries;

		for (int i = 0; i < numBoundaries; i++) {

			// Try to get the boundary rectangle with the most
			// coverage, so reset the entry for this particular query.
			entries[i].coverage.reset();

			if (chartSeriesCode == null) {
				chartSeries = RpfViewAttributes.ANY;
			} else {
				chartSeries = chartSeriesCode;
			}

			if (chartSeries.equalsIgnoreCase(RpfViewAttributes.ANY) || chartSeries.equalsIgnoreCase(
				entries[i].info.seriesCode)) {

				if (entries[i].coverage.setPercentCoverage(ullat, ullon, lrlat, lrlon) > 0f) {
					coverages.addElement(entries[i].coverage);
				}
			}
		}
	}

	/**
	 * Given a coordinate box and a scale, find the entry in the table of contents file with the right data. Zone is
	 * always of the northern hemisphere, and is transformed to southern inside if needed. The box will get filled in
	 * with the correct information. The subframe description will have scaling information for the subframes to be
	 * scaled to match the scale. If proj is null, only exact matches will be found
	 *
	 * NOTE: method getZone() of the CADRG projection is only relevant (according to OpenMap documentation) when you're
	 * viewing a map type (ONC, etc) at its proper scale (i.e. 1:1mil for ONC). There was a method in RpfTocHandler that
	 * only checked a TOC for coverage if the TOC zone matched the zone of the projection. This caused gaps of coverage
	 * when viewing the maps at large scales that were different from their proper scale (e.g. viewing JNC at 1:10mil).
	 * Modified this method so that it obtains all the possible zones the current map projection could be in, and
	 * compares the TOC zones to that set.
	 *
	 * Note that this now returns a list of coverage entries instead of just one.
	 *
	 * @param ullat upper left latitude, in decimal degrees
	 * @param ullon upper left longitude, in decimal degrees
	 * @param lrlat lower right latitude, in decimal degrees
	 * @param lrlon lower right longitude, in decimal degrees
	 * @param proj CADRG projection describing map.
	 * @param viewAtts view attributes determine chart selection.
	 * @return a Vector of applicable RpfCoverageBoxes.
	 */
	public List<RpfTocEntry> getBestCoverageEntry(double ullat, double ullon, double lrlat, double lrlon, Projection proj,
		RpfViewAttributes viewAtts) {
		if (!valid) {
			return null;
		}

		List<RpfTocEntry> coverageEntries = new Vector<RpfTocEntry>();
		double scaleFactor = 0;
		double lowerScaleFactorLimit = 1.0;
		double upperScaleFactorLimit = 1.0;

		// Good for a preliminary check. It has to start at least as
		// 4 to have one corner matching.
		int prevBoundaryHits = 0;

		if (viewAtts != null) {
			lowerScaleFactorLimit = (double) (1.0 / viewAtts.imageScaleFactor);
			upperScaleFactorLimit = (double) viewAtts.imageScaleFactor;
		}

		int nscale = 0;
		int scale = (int) proj.getScale();

		RpfTocEntry bestEntry = null;

		if (DEBUG_RPFTOCDETAIL) {
			Debug.output("getBestCoverageEntry(): Checking for coverage");
			Debug.output("  nw_lat: " + ullat);
			Debug.output("  se_lat: " + lrlat);
			Debug.output("  nw_lon: " + ullon);
			Debug.output("  se_lon: " + lrlon);
		}

		CADRG cadrg = CADRG.convertProjection(proj);

		int zone = getASCIIZone(ullat, cadrg.getZone());
		char okZones[] = getOkZones(ullat, lrlat, (char) zone);

		for (RpfTocEntry currentEntry : entries) {

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("********************");
				Debug.output("  tochandler: Boundary #" + currentEntry.coverage.entryNumber);
				Debug.output(currentEntry.toString());
			}

			// Try to get the boundary rectangle with the most
			// coverage, so reset the entry for this particular query.
			currentEntry.coverage.reset();

			// Find the scale of the boundary rectangle
			if (currentEntry.info == null || currentEntry.info.scale == RpfConstants.Various) {

				nscale = (int) textScaleToLong(currentEntry.scale);

				// Reset the RpfProductInfo to the listed parameters
				// in the A.TOC file.
				currentEntry.altScale = (float) nscale;
				currentEntry.altScaleString = currentEntry.scale;
				currentEntry.coverage.scale = (float) nscale;

			} else {
				currentEntry.coverage.scale = currentEntry.info.scale;
				nscale = (int) currentEntry.info.scale;
			}

			if (DEBUG_RPFTOCDETAIL) {
				Debug.output("getBestCoverageEntry(): Query scale = " + scale + " vs. brect scale = " + nscale);
			}

			// if you want an exact match for scale...
			if (viewAtts != null && !viewAtts.scaleImages) {
				if (scale == nscale) {
					scaleFactor = 1.0;
				} else {
					scaleFactor = lowerScaleFactorLimit - 1.0;
				}
			} else {
				scaleFactor = (double) nscale / (double) scale;
			}

			String chartSeries;
			if (viewAtts == null) {
				chartSeries = RpfViewAttributes.ANY;
			} else {
				chartSeries = viewAtts.chartSeries;
			}

			if (scaleFactor >= lowerScaleFactorLimit
				&& scaleFactor <= upperScaleFactorLimit
				&& (chartSeries.equalsIgnoreCase(RpfViewAttributes.ANY) || chartSeries.equalsIgnoreCase(
					currentEntry.info.seriesCode))) {

				if (ignoreZonesForCoverageBoxes || isOkZone(currentEntry.zone, okZones)) {
					// sets currentEntry.coverage.boundaryHits
					int hits = currentEntry.coverage.setBoundaryHits(ullat, ullon, lrlat, lrlon);

					if (DEBUG_RPFTOCDETAIL) {
						Debug.output("getBestCoverageEntry(): Boundary Hits = " + hits);
					}

					if (bestEntry != null) {

						boolean betterScale = false;

						float newScaleDiff = RpfFrameCacheHandler.scaleDifference(proj, currentEntry.coverage);
						float bestScaleDiff = RpfFrameCacheHandler.scaleDifference(proj, bestEntry.coverage);

						float currentEntryPercentCoverage = currentEntry.coverage
							.setPercentCoverage(ullat, ullon, lrlat, lrlon);

                        if ((currentEntryPercentCoverage == 100f
                                || (viewAtts != null && viewAtts.scaleMoreImportantThanCoverage))
							&& newScaleDiff <= bestScaleDiff) {
							betterScale = true;
						}

						if (betterScale
							&& (currentEntryPercentCoverage >= bestEntry.coverage.getPercentCoverage())
							&& (hits >= prevBoundaryHits || hits >= 6)) {

							// Add to list if has any hits and is
							// the best possible scale. If new scale
							// difference
							// is strictly better, remove other
							// entries
							if (newScaleDiff < bestScaleDiff) {
								coverageEntries.clear();
							}
							coverageEntries.add(currentEntry);

							bestEntry = currentEntry;
							prevBoundaryHits = hits;

							if (DEBUG_RPFTOC) {
								Debug.output("getBestCoverageEntry(): Found a match in a BR with coverage of "
									+ currentEntry.coverage.getPercentCoverage() + "%.");
							}
						} else if (betterScale && currentEntry.coverage.getPercentCoverage() > 0f) {

							if (newScaleDiff < bestScaleDiff) {
								coverageEntries.clear();
							}
							coverageEntries.add(currentEntry);
							bestEntry = currentEntry;
							prevBoundaryHits = hits;
						}

					} else if (hits > prevBoundaryHits
						&& (currentEntry.coverage.setPercentCoverage(ullat, ullon, lrlat, lrlon) > 0f)) {
						bestEntry = currentEntry;
						prevBoundaryHits = hits;

						// Add to list of coverageEntries
						coverageEntries.add(currentEntry);

						if (DEBUG_RPFTOC) {
							Debug.output("getBestCoverageEntry(): Found a match in a BR with coverage of "
								+ currentEntry.coverage.getPercentCoverage() + "%.");

						}
					}
				}
			}
		}

		if (DEBUG_RPFTOC) {
			if (bestEntry != null) {
				Debug.output("getBestCoverageEntry(): found the best");
				Debug.output("################");
				Debug.output(bestEntry.toString());
				Debug.output("Returning the following coverage boxes: ");

				for (int i = 0; i < coverageEntries.size(); i++) {
					Debug.output(coverageEntries.get(i).toString());
				}
			} else {
				Debug.output("getBestCoverageEntry(): no box found");
			}
		}

		return coverageEntries;
	}

	public static char[] getOkZones(double ullat, double lrlat, char zone) {
		// allow a maximum of 3 additional zones in either direction
		char[] okZones = new char[7];
		// add zone from projection
		okZones[0] = zone;
		// check above
		char currentZone = zone;
		char backupZone;
		int i = 0;
		for (; i < 3; i++) {
			if (isAboveZone(ullat, currentZone)) {
				backupZone = getHigherZone(currentZone);
				okZones[i + 1] = backupZone;
				currentZone = backupZone;
			} else {
				break;
			}
		}
		// check below
		int k = i;
		currentZone = zone;
		for (; k < i + 3; k++) {
			if (isBelowZone(ullat, currentZone)) {
				backupZone = getLowerZone(currentZone);
				okZones[k + 1] = backupZone;
				currentZone = backupZone;
			} else {
				break;
			}
		}
		int size = 0;
		for (int j = 0; j < okZones.length; j++) {
			if (okZones[j] != 0) {
				size++;
			}
		}
		char[] returnZones = new char[size];
		System.arraycopy(okZones, 0, returnZones, 0, size);
		return returnZones;
	}

	public static boolean isOkZone(char zone, char[] okZones) {
		boolean ok = false;
		for (int i = 0; i < okZones.length; i++) {
			if (zone == okZones[i]) {
				ok = true;
			}
		}
		return ok;
	}

	protected static boolean isBelowZone(double lowerLat, char zone) {
		float zoneLowerLat = getLowerZoneExtent(zone);
		if (lowerLat < zoneLowerLat) {
			return true;
		} else {
			return false;
		}
	}

	protected static boolean isAboveZone(double upperLat, char zone) {
		float zoneUpperLat = getUpperZoneExtent(zone);
		if (upperLat > zoneUpperLat) {
			return true;
		} else {
			return false;
		}
	}

	public static float getUpperZoneExtent(char zone) {

		if (zone >= '0' && zone <= '9') {
			int i = zone - 49;
			return CADRG_zone_extents[i + 1];
		} else {
			int i = zone - 65;
			if (i == 9) {
				i--; // Special care for j
			}
			return -1 * CADRG_zone_extents[i];
		}
	}

	public static float getLowerZoneExtent(char zone) {
		if (zone >= '0' && zone <= '9') {
			int i = zone - 49;
			return CADRG_zone_extents[i];
		} else {
			int i = zone - 65;
			if (i == 9) {
				i--; // Special care for J
			}
			return -1 * CADRG_zone_extents[i + 1];
		}
	}

	public static char getLowerZone(char zone) {
		// if zone = 'J' do nothing
		if (zone >= '2' && zone <= '9') {
			zone--;
		} else if (zone == '1') {
			zone = 'A';
		} else if (zone >= 'A' && zone < 'H') {
			zone++;
		} else if (zone == 'H') {
			zone = 'J';
		}
		return zone;
	}

	public static char getHigherZone(char zone) {
		// if zone = '9' do nothing
		if (zone >= '1' && zone < '9') {
			zone++;
		} else if (zone >= 'B' && zone < 'J' || zone >= 'b' && zone < 'j') {
			zone--;
		} else if (zone == 'J' || zone == 'j') {
			zone -= 2;
		} else if (zone == 'A' || zone == 'a') {
			zone = '1';
		}
		return zone;
	}

	/**
	 * Return the list of grouped frames.
	 */
	public RpfTocEntry[] getEntries() {
		return entries;
	}

	public String getATocFilePath() {
		return aTocFilePath;
	}

	public void setATocFilePath(String tocFilePath) {
		aTocFilePath = tocFilePath;
	}

	public boolean isFullPathsInATOC() {
		return fullPathsInATOC;
	}

	public void setFullPathsInATOC(boolean fullPathsInATOC) {
		this.fullPathsInATOC = fullPathsInATOC;
	}

	/**
	 * @return the ignoreZonesForCoverageBoxes
	 */
	public boolean isIgnoreZonesForCoverageBoxes() {
		return ignoreZonesForCoverageBoxes;
	}

	/**
	 * @param ignoreZonesForCoverageBoxes the ignoreZonesForCoverageBoxes to set
	 */
	public void setIgnoreZonesForCoverageBoxes(boolean ignoreZonesForCoverageBoxes) {
		this.ignoreZonesForCoverageBoxes = ignoreZonesForCoverageBoxes;
	}

	public static void main(String[] args) {
		if (args.length != 1) {
			Debug.output("Usage: java RpfTocHandler <path to RPF directory>");
			return;
		}

		Debug.init(System.getProperties());

		RpfTocHandler toc = new RpfTocHandler();
		if (!toc.loadFile(args[0])) {
			Debug.output("RpfTocHandler: NOT read successfully!");
		} else {
			RpfTocEntry[] e = toc.getEntries();
			Debug.output("For A.TOC: " + args[0]);
			for (int i = 0; i < e.length; i++) {
				Debug.output(e[i].toString());
			}
		}

		System.exit(0);
	}

}
